{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5fefabd2",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import math\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import datetime\n",
    "import time\n",
    "import random\n",
    "from datetime import date\n",
    "import pandas_ta as ta\n",
    "from ta.volatility import BollingerBands\n",
    "import matplotlib.pyplot as plt\n",
    "import plotly.graph_objects as go\n",
    "from plotly.subplots import make_subplots\n",
    "from yahoo_fin import stock_info as si\n",
    "import datetime\n",
    "from pandas.tseries.holiday import USFederalHolidayCalendar\n",
    "from pandas.tseries.offsets import CustomBusinessDay\n",
    "US_BUSINESS_DAY = CustomBusinessDay(calendar=USFederalHolidayCalendar())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c1ea6afe",
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_historic_data(symbol):\n",
    "    today = datetime.date.today()\n",
    "    today_str = today.strftime(\"%Y-%m-%d\")\n",
    "    #  Get last year's data\n",
    "    start_date = today - (251 * US_BUSINESS_DAY)\n",
    "    start_date_str = datetime.datetime.strftime(start_date, \"%Y-%m-%d\")\n",
    "    # Download data from Yahoo Finance\n",
    "    try:\n",
    "        df = si.get_data(symbol, start_date=start_date_str, end_date=today_str, index_as_date=False)\n",
    "        return df\n",
    "    except:\n",
    "        print('Error loading stock data for ' + symbol)\n",
    "        return None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c6d17621",
   "metadata": {},
   "outputs": [],
   "source": [
    "def calculate_bollinger_bands(df):\n",
    "    # Initialize Bollinger Bands Indicator\n",
    "    indicator_bb = BollingerBands(close=df[\"close\"], window=20, window_dev=2)\n",
    "\n",
    "    # Add Bollinger Bands features\n",
    "    df['BB_mid'] = indicator_bb.bollinger_mavg()\n",
    "    df['BB_high'] = indicator_bb.bollinger_hband()\n",
    "    df['BB_low'] = indicator_bb.bollinger_lband()\n",
    "\n",
    "    return df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1341e115",
   "metadata": {},
   "outputs": [],
   "source": [
    "def apply_strategy_rules(df):\n",
    "    #  Entry Rule 1: Close price below Low Bollinger Band\n",
    "    df['BB_entry_signal'] = np.where((df[\"close\"] < df[\"BB_low\"]) & (df[\"close\"].shift() >= df[\"BB_low\"]), 1, 0)\n",
    "    \n",
    "    #  Exit rule\n",
    "    df['BB_exit_signal'] = np.where((df[\"close\"] > df[\"BB_high\"]) & (df[\"close\"].shift() <= df[\"BB_high\"]), 1, 0)\n",
    "\n",
    "    return df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d60cc7d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "def execute_strategy(df):\n",
    "    close_prices = df['close']\n",
    "    BB_entry_signals = df['BB_entry_signal']\n",
    "    BB_exit_signals = df['BB_exit_signal']\n",
    "    entry_prices = []\n",
    "    exit_prices = []\n",
    "    entry_signal = 0\n",
    "    exit_signal = 0\n",
    "    buy_price = -1\n",
    "    hold = 0\n",
    "    \n",
    "    for i in range(len(close_prices)):\n",
    "        #  Check entry and exit signals\n",
    "        if BB_entry_signals[i] == 1:\n",
    "            entry_signal = 1\n",
    "        else:\n",
    "            entry_signal = 0\n",
    "        if BB_exit_signals[i] == 1:\n",
    "            exit_signal = 1\n",
    "        else:\n",
    "            exit_signal = 0\n",
    "            \n",
    "        #  Add entry prices\n",
    "        if hold == 0 and entry_signal == 1:\n",
    "            buy_price = close_prices[i]\n",
    "            entry_prices.append(close_prices[i])\n",
    "            exit_prices.append(np.nan)  \n",
    "            entry_signal = 0\n",
    "            hold = 1\n",
    "        #  Evaluate exit strategy\n",
    "        elif (hold == 1 and exit_signal == 1 or (hold == 1 and close_prices[i] <= buy_price * 0.95)):\n",
    "            entry_prices.append(np.nan)\n",
    "            exit_prices.append(close_prices[i]) \n",
    "            exit_signal = 0\n",
    "            buy_price = -1\n",
    "            hold = 0\n",
    "        else:\n",
    "            #  Neither entry nor exit\n",
    "            entry_prices.append(np.nan) \n",
    "            exit_prices.append(np.nan) \n",
    "            \n",
    "    return entry_prices, exit_prices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "97160be4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_graph(df, entry_prices, exit_prices):\n",
    "    bb_high = df['BB_high']\n",
    "    bb_mid = df['BB_mid']\n",
    "    bb_low = df['BB_low']\n",
    "    fig = make_subplots(rows=1, cols=1)\n",
    "\n",
    "    #  Plot close price\n",
    "    fig.add_trace(go.Line(x = df.index, y = df['close'], line=dict(color=\"blue\", width=1), name=\"Close\"), row = 1, col = 1)\n",
    "    \n",
    "    #  Plot bollinger bands\n",
    "    fig.add_trace(go.Line(x = df.index, y = bb_high, line=dict(color=\"#ffdf80\", width=1), name=\"BB High\"), row = 1, col = 1)\n",
    "    fig.add_trace(go.Line(x = df.index, y = bb_mid, line=dict(color=\"#ffd866\", width=1), name=\"BB Mid\"), row = 1, col = 1)\n",
    "    fig.add_trace(go.Line(x = df.index, y = bb_low, line=dict(color=\"#ffd24d\", width=1), name=\"BB Low\"), row = 1, col = 1)\n",
    "    \n",
    "    #  Add buy and sell indicators\n",
    "    fig.add_trace(go.Scatter(x=df.index, y=entry_prices, marker_symbol=\"arrow-up\", marker=dict(\n",
    "        color='green',\n",
    "    ),mode='markers',name='Buy'))\n",
    "    fig.add_trace(go.Scatter(x=df.index, y=exit_prices, marker_symbol=\"arrow-down\", marker=dict(\n",
    "        color='red'\n",
    "    ),mode='markers',name='Sell'))\n",
    "    \n",
    "    fig.update_layout(\n",
    "        title={'text':'BB + Stop Loss', 'x':0.5},\n",
    "        autosize=False,\n",
    "        width=800,height=400)\n",
    "    fig.update_yaxes(range=[0,1000000000],secondary_y=True)\n",
    "    fig.update_yaxes(visible=False, secondary_y=True)  #hide range slider\n",
    "    \n",
    "    fig.show()\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a501873",
   "metadata": {},
   "outputs": [],
   "source": [
    "def calculate_buy_hold_profit(investment, df):\n",
    "    close_prices = df['close']\n",
    "    buy_quantity = investment / close_prices[0]\n",
    "    sell_amount = buy_quantity * close_prices[len(close_prices)-1]\n",
    "    profit = sell_amount - investment\n",
    "    return profit\n",
    "   "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84b8285c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def calculate_strategy_profit(investment, entry_prices, exit_prices):\n",
    "    entry_price = 0\n",
    "    hold = 0\n",
    "    total_profit = 0\n",
    "    quantity = 0\n",
    "    available_funds = investment\n",
    "    purchase_amount = 0\n",
    "    \n",
    "    for i in range(len(entry_prices)):\n",
    "        current_entry_price = entry_prices[i]\n",
    "        current_exit_price = exit_prices[i]\n",
    "        \n",
    "        if not math.isnan(current_entry_price) and hold == 0:\n",
    "            entry_price = current_entry_price\n",
    "            quantity = available_funds / entry_price\n",
    "            purchase_amount = quantity * entry_price\n",
    "            hold = 1\n",
    "        elif hold == 1 and not math.isnan(current_exit_price):\n",
    "            hold = 0\n",
    "            sales_amount = quantity * current_exit_price\n",
    "            profit_or_loss = sales_amount - purchase_amount\n",
    "            available_funds = available_funds + profit_or_loss\n",
    "            total_profit += profit_or_loss\n",
    "        \n",
    "    return total_profit        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cc8bdd7a",
   "metadata": {},
   "outputs": [],
   "source": [
    "#  Perform analysis\n",
    "investment = 1000\n",
    "df = load_historic_data('BKNG')\n",
    "df.reset_index(inplace=True)\n",
    "df = calculate_bollinger_bands(df)\n",
    "df = apply_strategy_rules(df)\n",
    "entry_prices, exit_prices = execute_strategy(df)\n",
    "profit_or_loss = calculate_strategy_profit(investment, entry_prices, exit_prices)\n",
    "buy_hold_profit = calculate_buy_hold_profit(investment, df)\n",
    "plot_graph(df, entry_prices, exit_prices)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "88ecd586",
   "metadata": {},
   "outputs": [],
   "source": [
    "def perform_analysis(symbol, df, investment):\n",
    "    df = df.reset_index()\n",
    "    df = calculate_bollinger_bands(df)\n",
    "    df = apply_strategy_rules(df)\n",
    "    \n",
    "    entry_prices, exit_prices = execute_strategy(df)\n",
    "    profit_or_loss = calculate_strategy_profit(investment, entry_prices, exit_prices)\n",
    "    buy_hold_profit = calculate_buy_hold_profit(investment, df)\n",
    "    return profit_or_loss, buy_hold_profit"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8bc1b36f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Backtesting using NASDAQ 100\n",
    "nasdaq_100_df = pd.read_csv('https://raw.githubusercontent.com/justmobiledev/python-algorithmic-trading/main/data/nasdaq_100.csv')\n",
    "nasdaq_100 = nasdaq_100_df['Symbol'].to_numpy()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6600aba0",
   "metadata": {},
   "outputs": [],
   "source": [
    "#  Backtesting\n",
    "total_strategy_profit = 0\n",
    "total_buy_hold_profit = 0\n",
    "for symbol in nasdaq_100:\n",
    "    df = load_historic_data(symbol)\n",
    "    if df is None or df.empty:\n",
    "        continue\n",
    "    df.reset_index(inplace=True)\n",
    "    \n",
    "    #  Random interval between remote fetch to avoid spam issues\n",
    "    random_secs = random.uniform(0, 1)\n",
    "    time.sleep(random_secs)\n",
    "    \n",
    "    #  Run backtest\n",
    "    profit, buy_hold_profit = perform_analysis(symbol, df, investment=investment) \n",
    "    print(f\"Backtest profit for symbol {symbol}: ${math.trunc(profit)}, buy & hold: ${math.trunc(buy_hold_profit)}\")\n",
    "    total_strategy_profit += profit\n",
    "    total_buy_hold_profit += buy_hold_profit\n",
    "  \n",
    "print(f\"\\nAvg strategy profit per stock: ${math.trunc(total_strategy_profit / len(nasdaq_100))}\")\n",
    "print(f\"\\nAvg buy & hold profit per stock: ${math.trunc(total_buy_hold_profit / len(nasdaq_100))}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:tradesystem1]",
   "language": "python",
   "name": "conda-env-tradesystem1-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
